import {
  Cartesian2,
  Cartesian3,
  Cartographic,
  Cesium3DTileFeature,
  Cesium3DTileset,
  Math as CesiumMath,
  EllipsoidTerrainProvider,
  Model,
  Scene,
  Viewer,
  Ray,
  Rectangle,
  Globe
} from 'cesium';

interface LngLatHeight {
  longitude: number;
  latitude: number;
  height: number;
}

interface LonLat {
  lon: number;
  lat: number;
}

export class Coord {
  private viewer: Viewer;
  private scene: Scene;
  private globe: Globe;

  constructor(viewer: Viewer) {
    this.viewer = viewer;
    this.scene = viewer.scene;
    this.globe = viewer.scene.globe;
  }

  cartesian2ToCartesian3(position: Cartesian2): Cartesian3 | null {
    if (!this.viewer || !position) return null;

    const picks = this.scene.drillPick(position);
    let isOn3dtiles = false;

    for (const i in picks) {
      if (!Number.isNaN(Number(i))) {
        const pick = picks[i];
        isOn3dtiles =
          pick?.primitive instanceof Cesium3DTileFeature || pick?.primitive instanceof Cesium3DTileset || pick?.primitive instanceof Model;

        if (isOn3dtiles) {
          this.scene.pick(position);
          const cartesian3 = this.scene.pickPosition(position);

          if (cartesian3) {
            const cartographic = Cartographic.fromCartesian(cartesian3);
            if (cartographic.height < 0) cartographic.height = 0;
            const lon = CesiumMath.toDegrees(cartographic.longitude);
            const lat = CesiumMath.toDegrees(cartographic.latitude);
            const height = cartographic.height;
            return this.lnglatToCartesian3(lon, lat, height);
          }
        }
      }
    }

    // 不在模型上
    if (!isOn3dtiles) {
      const isTerrain = this.viewer.terrainProvider instanceof EllipsoidTerrainProvider;

      if (!isTerrain) {
        // 无地形
        const ray: Ray | undefined = this.scene.camera.getPickRay(position);
        if (!ray) return null;
        return this.globe.pick(ray, this.scene);
      } else {
        const cartesian3 = this.scene.camera.pickEllipsoid(position, this.globe.ellipsoid);
        if (cartesian3) {
          const position = this.cartesian3ToLngLat(cartesian3);
          if (position && position.height < 0) {
            return this.lnglatToCartesian3(position.longitude, position.latitude, 0.1);
          }
          return cartesian3;
        }
      }
    }

    return null;
  }

  cartesian2ToLnglat(position: Cartesian2): LngLatHeight | undefined {
    const cartesian3 = this.cartesian2ToCartesian3(position);
    return cartesian3 ? this.cartesian3ToLngLat(cartesian3) : undefined;
  }

  cartesian3ToCartesian2(cartesian3: Cartesian3): Cartesian2 | undefined {
    return this.scene.cartesianToCanvasCoordinates(cartesian3);
  }

  computeViewerBounds(): number[] {
    const extend: Rectangle | undefined = this.scene.camera.computeViewRectangle();
    let bounds: number[] = [];

    if (typeof extend === 'undefined') {
      const coordToLonlat = (viewer: Viewer, x: number, y: number): LonLat | undefined => {
        const { camera, scene } = viewer;
        const d2 = new Cartesian2(x, y);
        const ellipsoid = scene.globe.ellipsoid;
        const d3 = camera.pickEllipsoid(d2, ellipsoid);

        if (d3) {
          const upperLeftCartographic = ellipsoid.cartesianToCartographic(d3);
          const lon = CesiumMath.toDegrees(upperLeftCartographic.longitude);
          const lat = CesiumMath.toDegrees(upperLeftCartographic.latitude);
          return { lon, lat };
        }
        return undefined;
      };

      const canvas = this.scene.canvas;
      const upperLeftLonLat = coordToLonlat(this.viewer, 0, 0);
      const lowerRightLonLat = coordToLonlat(this.viewer, canvas.clientWidth, canvas.clientHeight);

      if (upperLeftLonLat?.lon && upperLeftLonLat?.lat && lowerRightLonLat?.lon && lowerRightLonLat?.lat) {
        bounds = [upperLeftLonLat.lon, upperLeftLonLat.lat, lowerRightLonLat.lon, lowerRightLonLat.lat];
      }
    } else {
      bounds = [
        CesiumMath.toDegrees(extend.west),
        CesiumMath.toDegrees(extend.south),
        CesiumMath.toDegrees(extend.east),
        CesiumMath.toDegrees(extend.north)
      ];
    }

    return bounds;
  }

  isVisibleByBounds(position: Cartesian3): boolean {
    const bounds = this.computeViewerBounds();
    if (!bounds.length) return false;

    const lnglat = this.cartesian3ToLngLat(position);
    if (!lnglat) return false;

    const { longitude, latitude } = lnglat;
    return longitude >= bounds[0] && longitude <= bounds[2] && latitude >= bounds[1] && latitude <= bounds[3];
  }

  cartesian3ToLngLat(cartesian3: Cartesian3): LngLatHeight | undefined {
    if (!cartesian3) return undefined;

    const radians = this.globe.ellipsoid.cartesianToCartographic(cartesian3);
    if (!radians) return undefined;

    return {
      longitude: CesiumMath.toDegrees(radians.longitude),
      latitude: CesiumMath.toDegrees(radians.latitude),
      height: radians.height
    };
  }

  lnglatToCartesian3(longitude: number, latitude: number, height: number): Cartesian3 {
    return Cartesian3.fromDegrees(longitude, latitude, height);
  }
}
